Form 로그인 및 로그아웃 테스트

✒️ 2025-07-02 11:26 내용 수정


FormLogin 동작

로그인 과정

  1. 사용자가 인증되지 않은 HTTP 요청을 전송한다.
  2. AuthorizationFilter가 인증되지 않은 요청을 차단한다.
    • AccessDeniedException 예외를 던진다.
  3. ExceptionTranslationFilter에서 인증을 시작하고, AuthenticationEntryPoint로 식별된 로그인 페이지로 리다이렉션한다.
  4. 브라우저는 이를 수신하여 사용자를 로그인 페이지로 보낸다.
  5. 사용자가 usernmaepassword를 입력하여 POST 요청을 전송한다.
  6. UsernamePasswordAuthenticationFilter에서 HttpServletRequest 인스턴스에 포함된 usernamepassword를 추출하여 Authentication 종류 중 하나인 UsernamePasswordAuthenticationToken을 생성한다.
  7. UsernamePasswordAuthenticationTokenAuthenticationManager 객체에 전달한다.
  8. AuthenticationManager는 인증 성공 및 실패 시 동작을 처리한다.
    1. 인증 실패 시
      • SecurityContextHolder를 비운다.
      • RememberMe를 구성했었다면 RememberMeServices.loginFail이 호출된다.
      • AuthenticationFailureHandler가 호출되어 로그인 실패 후 동작을 처리한다.
    2. 인증 성공 시
      • SessinoAuthenticationStrategy가 새 로그인을 인지한다.
      • SecurityContextPersistenceFilterAuthenticationSecurityContextHolder에 저장한다.
      • RememberMe를 구성했다면 RememberMeServices.loginSuccess가 호출된다.
      • ApplicationEventPublisherInteractiveAuthenticationSuccessEvent를 만든다.
      • AuthenticationSuccessHandler가 호출되어 로그인 성공 후 동작을 처리한다.

기본 Login Form 설정 시 keypoint

<!-- thymeleaf 적용 -->
<html xmlns:th="http://www.thymeleaf.org">
<body>
	<!-- 중간 생략 -->
	<form th:action="@{/login}" method="post"></form>
</body>
</html>
<form th:action="@{/login}" method="post">
	<input type="hidden" 
		th:name="${_csrf.parameterName}" 
		th:value="${_csrf.token}"/>
</form>
<form th:action="@{/login}" method="post">
	<input type="hidden" 
		th:name="${_csrf.parameterName}" 
		th:value="${_csrf.token}"/>
		
	<input type="text" name="username"/>
	<input type="password" name="password"/>
</form>
<div th:if="${param.error}">  
    <p style="color:red">로그인 정보가 올바르지 않습니다</p>  
</div>  
<div th:if="${param.logout}">  
    <p style="color:green">정상적으로 로그아웃 되었습니다.</p>  
</div>

<form th:action="@{/login}" method="post">
	<input type="hidden" 
		th:name="${_csrf.parameterName}" 
		th:value="${_csrf.token}"/>
		
	<input type="text" name="username"/>
	<input type="password" name="password"/>
</form>

Logout


실습 프로젝트

spring_boot_security_formlogin 0.png

의존성 설정

<dependencies>  
	<!-- Spring Security -->
	<dependency>       
		<groupId>org.springframework.boot</groupId>  
		<artifactId>spring-boot-starter-security</artifactId>  
	</dependency>  
	<!-- Thymeleaf -->
	<dependency>       
		<groupId>org.springframework.boot</groupId>  
		<artifactId>spring-boot-starter-thymeleaf</artifactId>  
	</dependency>  
	<!-- Spring Web -->
	<dependency>       
		<groupId>org.springframework.boot</groupId>  
		<artifactId>spring-boot-starter-web</artifactId>  
	</dependency>  
	<!-- Spring Security + Thyemleaf -->
	<dependency>       
		<groupId>org.thymeleaf.extras</groupId>  
		<artifactId>thymeleaf-extras-springsecurity6</artifactId>  
	</dependency>  

	<!-- Spring Test -->
	<dependency>       
		<groupId>org.springframework.boot</groupId>  
		<artifactId>spring-boot-starter-test</artifactId>  
		<scope>test</scope>  
	</dependency>  
	<dependency>       
		<groupId>org.springframework.security</groupId>  
		<artifactId>spring-security-test</artifactId>  
	<scope>test</scope>  
	</dependency>  
	
	<!-- H2 Database -->
	<dependency>       
		<groupId>com.h2database</groupId>  
		<artifactId>h2</artifactId>  
		<version>2.3.230</version>  
	</dependency>  
</dependencies>

Controller 설정

// MainController
import org.springframework.stereotype.Controller;  
import org.springframework.ui.Model;  
import org.springframework.web.bind.annotation.GetMapping;  
  
import java.security.Principal;  
  
@Controller  
public class MainController {  
    @GetMapping({"/", "/home"})  
    public String home(Principal principal, Model model) {  
        model.addAttribute("username", principal.getName());  
        return "home";  
    }  
  
    @GetMapping("/admin")  
    public String admin(Principal principal, Model model) {  
        model.addAttribute("username", principal.getName());  
        return "admin";  
    }  
  
    @GetMapping("/login")  
    public String login() {  
        return "login";  
    }  
}

SecurityConfig 설정

@Configuration  
@EnableWebSecurity // Spring Security 자동 활성화  
public class SecurityConfig { }
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { }
// 인메모리 DB 설정  
// 사용자 객체를 인메모리 DB에 추가  
@Bean  
public InMemoryUserDetailsManager userDetailsManager() {  
	UserDetails user = User.withDefaultPasswordEncoder()  
			.username("user")  
			.password("1234")  
			.roles("USER")  
			.build();  

	UserDetails admin = User.withDefaultPasswordEncoder()  
			.username("admin")  
			.password("aaaa")  
			.roles("ADMIN")  
			.build();  

	return new InMemoryUserDetailsManager(user, admin);  
}
// SecurityConfig
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.security.config.Customizer;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;  
import org.springframework.security.core.userdetails.User;  
import org.springframework.security.core.userdetails.UserDetails;  
import org.springframework.security.provisioning.InMemoryUserDetailsManager;  
import org.springframework.security.web.SecurityFilterChain;  
  
@Configuration  
@EnableWebSecurity
public class SecurityConfig {  
  
    // 인메모리 DB 설정  
    // 사용자 객체를 인메모리 DB에 추가  
    @Bean  
    public InMemoryUserDetailsManager userDetailsManager() {  
        UserDetails user = User.withDefaultPasswordEncoder()  
                .username("user")  
                .password("1234")  
                .roles("USER")  
                .build();  
  
        UserDetails admin = User.withDefaultPasswordEncoder()  
                .username("admin")  
                .password("aaaa")  
                .roles("ADMIN")  
                .build();  
  
        return new InMemoryUserDetailsManager(user, admin);  
    }  
  
    // Filter Chain 설정  
    @Bean  
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {  
        http  
                // HTTP 요청의 인가 설정  
                // URL별 접근 설정  
                .authorizeHttpRequests(auth->auth  
                        .requestMatchers("/login", "/error").permitAll()  
                        .requestMatchers("/admin").hasRole("ADMIN")  
                        .anyRequest().authenticated()  
                )  
                // form을 사용한 로그인 설정  
                .formLogin(form->form  
                        .loginPage("/login") // 로그인 페이지  
                        .defaultSuccessUrl("/home", true) // 로그인 성공 시 URL                        
                        .permitAll() // 접근 권한 모두 허용  
                )  
                // 로그아웃  
                .logout(logout->logout  
						.logoutUrl("/logout") // 로그아웃 요청 URL                        
						.logoutSuccessUrl("/login?logout") // 로그아웃 성공 시 URL                        
                        .invalidateHttpSession(true) // HTTP Session 무효화  
                        .deleteCookies("JSESSIONID") // 쿠키 제거  
                        .permitAll() // 접근 권한 모두 허용  
                )  
                .httpBasic(Customizer.withDefaults()); // 인증, 인가 테스트  
        return http.build();  
    }  
}

view 설정

1) home.html

<!doctype html>  
<html lang="ko" xmlns:th="http://www.thymeleaf.org">  
<head>  
    <meta charset="UTF-8">  
    <title>Home</title>  
</head>  
<body>  
    <h1>반갑습니다. <span th:text="${username}"></span>님</h1>  
    <a th:href="@{/admin}">관리자 페이지로 이동</a>  
    <form th:action="@{/logout}" method="post" style="display:inline;">  
        <input type="hidden" 
        th:name="${_csrf.parameterName}" th:value="${_csrf.token}">  
        <button type="submit">로그아웃</button>  
    </form>  
</body>  
</html>

2) login.html

<!doctype html>  
<html lang="ko" xmlns:th="http://www.thymeleaf.org">  
<head>  
    <meta charset="UTF-8">  
    <title>Login</title>  
</head>  
<body>  
    <form th:action="@{/login}" method="post">  
        <div>  
            <label>이름: </label>  
            <input type="text" name="username">  
        </div>  
        <div>            
	        <label>비밀번호: </label>  
            <input type="password" name="password">  
        </div>  
        <div>            
	        <button type="submit">로그인</button>  
        </div>  
        <div th:if="${param.error}">  
            <p style="color:red">로그인 정보가 올바르지 않습니다</p>  
        </div>  
        <div th:if="${param.logout}">  
            <p style="color:green">정상적으로 로그아웃 되었습니다.</p>  
        </div>  
    </form>  
</body>  
</html>

3) admin.html

<!doctype html>  
<html lang="ko" xmlns:th="http://www.thymeleaf.org">  
<head>  
    <meta charset="UTF-8">  
    <title>Admin</title>  
</head>  
<body>  
    <h1>관리자 페이지</h1>  
    <p>관리자 <span th:text="${username}"></span>님 환영합니다</p>  
    <a th:href="@{/home}">홈으로 이동</a>  
    <form th:action="@{/logout}" method="post" style="display:inline;">  
        <input type="hidden" 
	        th:name="${_csrf.parameterName}" 
	        th:value="${_csrf.token}">  
        <button type="submit">로그아웃</button>  
    </form>  
</body>  
</html>

4) error.html

<!doctype html>  
<html lang="ko" xmlns:th="http://www.thymeleaf.org">  
<head>  
    <meta charset="UTF-8">  
    <title>Error</title>  
</head>  
<body>  
    <h1>오류가 발생했습니다.</h1>  
    <a th:href="@{/home}">홈으로 이동</a>  
</body>  
</html>

테스트

  1. 일반 사용자 로그인 테스트

spring_boot_security_formlogin 1.png

spring_boot_security_formlogin 2.png

  1. 로그아웃 테스트

spring_boot_security_formlogin 3.png

  1. 관리자 로그인 테스트

spring_boot_security_formlogin 4.png

spring_boot_security_formlogin 5.png

  1. 관리자 페이지 접근 확인

spring_boot_security_formlogin 6.png